﻿//
// $HeadURL: http://codeka-memcache.googlecode.com/svn/trunk/Codeka.Memcache/MemcacheClient.cs $
// $Revision: 11 $
// $Author: dean.codeka $     $Date: 2010-02-21 08:02:52 +0000 (Sun, 21 Feb 2010) $
//
// Copyright © 2009-2010 Dean Harding, all rights reserved.
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Codeka.Memcache.Commands;
using Codeka.Memcache.Config;

namespace Codeka.Memcache
{
    /// <summary>
    /// This is the main interface to the Codeka-Memcache client. Simply
    /// create an instance of this class (with appropriate configuration
    /// information) and call <b>Add</b>, <b>Set</b>, <b>Get</b>, etc.
    /// </summary>
    public class MemcacheClient
    {
        private static IMemcacheConfiguration defaultConfiguration;
        private static ServerPoolMap serverPoolMap;
        private IMemcacheConfiguration config;
        private ServerPool serverPool;

        /// <summary>
        /// Static constuctor sets up the default configuration instance.
        /// </summary>
        static MemcacheClient()
        {
            defaultConfiguration = new DefaultConfiguration();
            serverPoolMap = new ServerPoolMap();
        }

        /// <summary>
        /// You can use this to set the <see cref="IMemcacheConfiguration"/> instance
        /// we'll use if you use the <see cref="MemcacheClient"/> constructor with
        /// no parameters. This is slightly easier than ensuring you pass the correct
        /// configuration every time you construct a new <see cref="MemcacheClient"/>.
        /// </summary>
        /// <param name="config">The <see cref="IMemcacheConfiguration"/> instance
        /// to use when you construct the <see cref="MemcacheClient"/> class using the
        /// parameterless constructor.</param>
        public static void SetDefaultConfiguration(IMemcacheConfiguration config)
        {
            defaultConfiguration = config;
        }

        /// <summary>
        /// Constructs a new <see cref="MemcacheClient"/> with the default configuration.
        /// </summary>
        /// <remarks>
        /// <para>You can change the configuration we use here by calling the
        /// <see cref="SetDefaultConfiguration"/> method.</para>
        /// </remarks>
        public MemcacheClient()
            : this(defaultConfiguration)
        {
        }

        /// <summary>
        /// Constructs a new <see cref="MemcacheClient"/> that will use the specified
        /// <see cref="IMemcacheConfiguration"/> for it's configuration.
        /// </summary>
        /// <param name="config">The <see cref="IMemcacheConfiguration"/> to use for
        /// configuration.</param>
        /// <remarks>
        /// <para>Rather than call this overload in multiple places, it's usually better
        /// to just call <see cref="SetDefaultConfiguration"/> and use the parameterless
        /// constructor instead (otherwise, you're liable to forget sometimes).</para>
        /// </remarks>
        public MemcacheClient(IMemcacheConfiguration config)
        {
            this.config = config;
            this.serverPool = serverPoolMap.GetServerPool(config);
        }

        #region GetServerInfo
        /// <summary>
        /// Begins the "GetServerInfo" operation. We enqueue a "GetServerInfo" request
        /// to each server and call the given <c>callback</c> when they've all returned
        /// their data.
        /// </summary>
        /// <param name="callback">A callback to call when the data is available.</param>
        /// <param name="state">The <c>state</c> object representing some application-specific
        /// state that you want returned in your callback.</param>
        /// <returns>An <see cref="IAsyncResult"/> you can use to query the progress/result of
        /// the operation.</returns>
        public IAsyncResult BeginGetServerInfo(AsyncCallback callback, object state)
        {
            var commands = new List<KeyValuePair<ServerConnection, Command>>();
            foreach (var svrConnection in serverPool.GetServers())
            {
                commands.Add(new KeyValuePair<ServerConnection, Command>(svrConnection, new GetServerInfoCommand()));
            }

            return BeginExecuteCommands(commands, callback, state);
        }

        /// <summary>
        /// The synchronous version of <see cref="BeginGetServerInfo"/>.
        /// </summary>
        /// <returns>A <see cref="List{T}"/> of all the <see cref="ServerInfo"/>s.</returns>
        public List<ServerInfo> GetServerInfo()
        {
            // we're just a simple wrapper around the Begin/End method(s)
            var res = BeginGetServerInfo(null, null);
            return EndGetServerInfo(res);
        }

        /// <summary>
        /// When we call your callback you passed to <see cref="BeginGetServerInfo"/>, you
        /// call this method to actually get the result.
        /// </summary>
        /// <param name="res">The <see cref="IAsyncResult"/> object.</param>
        /// <returns>A <see cref="List{T}"/> of <see cref="ServerInfo"/>s, representing details
        /// about each of the memcache servers we're connected to.</returns>
        public List<ServerInfo> EndGetServerInfo(IAsyncResult res)
        {
            var infos = new List<ServerInfo>();
            foreach (var kvp in EndExecuteCommands(res))
            {
                var cmd = (GetServerInfoCommand)kvp.Value;
                infos.Add(cmd.ServerInfo);
            }
            return infos;
        }
        #endregion

        #region Store
        /// <summary>
        /// Begins a "Store" operation that'll store the given value at the given key for the given time. We
        /// use the default <see cref="StoreMode"/> of <see cref="StoreMode.Set"/>.
        /// </summary>
        /// <param name="key">The key to store the value at.</param>
        /// <param name="value">The actual value to store.</param>
        /// <param name="expiry">A <see cref="TimeSpan"/> representing the amount of time until the object
        /// expires (<b>TimeSpan.Zero</b> can be used to indicate no expiry).</param>
        /// <param name="callback">An <see cref="AsyncCallback"/> to call when the operation is complete.</param>
        /// <param name="state">A state object to pass back to the callback when we're finished.</param>
        public IAsyncResult BeginStore(string key, object value, TimeSpan expiry, AsyncCallback callback, object state)
        {
            return BeginStore(StoreMode.Set, key, value, expiry, callback, state);
        }

        /// <summary>
        /// Stores the value with the given key and expiry.
        /// </summary>
        /// <param name="key">The key to store the value at.</param>
        /// <param name="value">The value to store.</param>
        /// <param name="expiry">The expiry time you want to store the value for (<b>TimeSpan.Zero</b>
        /// can be used to indicate no expiry).</param>
        public StoreResult Store(string key, object value, TimeSpan expiry)
        {
            return Store(StoreMode.Set, key, value, expiry);
        }

        /// <summary>
        /// Stores an object with the given <see cref="StoreMode"/>, key, value and expiry time.
        /// </summary>
        /// <param name="storeMode">The <see cref="StoreMode"/> to store the value with.</param>
        /// <param name="key">The key to store the value at.</param>
        /// <param name="value">The actual value to store.</param>
        /// <param name="expiry">A <see cref="TimeSpan"/> representing the amount of time until the object
        /// expires (<b>TimeSpan.Zero</b> can be used to indicate no expiry).</param>
        /// <param name="callback">An <see cref="AsyncCallback"/> to call when the operation is complete.</param>
        /// <param name="state">A state object to pass back to the callback when we're finished.</param>
        public IAsyncResult BeginStore(StoreMode storeMode, string key, object value, TimeSpan expiry, AsyncCallback callback, object state)
        {
            return BeginStore(new List<KeyValuePair<string, StoreObject>> {
                    new KeyValuePair<string, StoreObject>(key, new StoreObject { Expiry = expiry, StoreMode = storeMode, Flags = 0, Value = value }),
                }, callback, state);
        }

        /// <summary>
        /// Stores the value with the given key and expiry.
        /// </summary>
        /// <param name="storeMode">The <see cref="StoreMode"/> to store the value with.</param>
        /// <param name="key">The key to store the value at.</param>
        /// <param name="value">The value to store.</param>
        /// <param name="expiry">The expiry time you want to store the value for (<b>TimeSpan.Zero</b>
        /// can be used to indicate no expiry).</param>
        public StoreResult Store(StoreMode storeMode, string key, object value, TimeSpan expiry)
        {
            var res = BeginStore(storeMode, key, value, expiry, null, null);
            var results = EndStore(res);

            StoreResult result;
            if (results.TryGetValue(key, out result))
                return result;

            // if the EndStore didn't return anything, then it means it was successful
            return StoreResult.Success;
        }

        /// <summary>
        /// Begins the "Store" operation. We will store the give collection of values with
        /// the give keys and expiry time(s).
        /// </summary>
        /// <param name="objects">An <see cref="IEnumerable{T}"/> collection of 
        /// <see cref="KeyValuePair"/>s (e.g. a dictionary) that maps keys to the values you want
        /// to store at that key.</param>
        /// <param name="callback">A callback to call when the data is available.</param>
        /// <param name="state">The <c>state</c> object representing some application-specific
        /// state that you want returned in your callback.</param>
        /// <returns>An <see cref="IAsyncResult"/> you can use to query the progress/result of
        /// the operation.</returns>
        public IAsyncResult BeginStore(IEnumerable<KeyValuePair<string, StoreObject>> objects, AsyncCallback callback, object state)
        {
            var commands = new List<KeyValuePair<ServerConnection, Command>>();
            foreach (var kvp in objects)
            {
                var svr = serverPool.GetServer(kvp.Key);
                commands.Add(new KeyValuePair<ServerConnection, Command>(svr, new StoreCommand(kvp)));
            }

            return BeginExecuteCommands(commands, callback, state);
        }

        /// <summary>
        /// Completes a "store" command. Returns a mapping of key->results so you can tell which
        /// key(s) were successfully stored, and which (if any) were not. For values that are
        /// <em>successfully</em> stored, nothing is returned in the dictionary.
        /// </summary>
        /// <param name="res">The <see cref="IAsyncResult"/> object.</param>
        /// <returns>An <see cref="IDictionary{TKey,TValue}"/> which maps keys to a 
        /// <see cref="StoreResult"/> if there was a problem storing the value.</returns>
        public IDictionary<string, StoreResult> EndStore(IAsyncResult res)
        {
            var results = new Dictionary<string, StoreResult>();
            foreach (var kvp in EndExecuteCommands(res))
            {
                var cmd = (StoreCommand)kvp.Value;
                foreach (var resultKvp in cmd.Results)
                {
                    if (resultKvp.Value != StoreResult.Success)
                    {
                        results.Add(resultKvp.Key, resultKvp.Value);
                    }
                }
            }
            return results;
        }
        #endregion

        #region Get
        /// <summary>
        /// Begins the "Get" operation. We get the value(s) associated with each of the passed-in
        /// keys, if available.
        /// </summary>
        /// <param name="keys">The <see cref="IEnumerable{T}"/> collection of keys to fetch.</param>
        /// <param name="callback">A callback to call when the data is available.</param>
        /// <param name="state">The <c>state</c> object representing some application-specific
        /// state that you want returned in your callback.</param>
        /// <returns>An <see cref="IAsyncResult"/> you can use to query the progress/result of
        /// the operation.</returns>
        public IAsyncResult BeginGet(IEnumerable<string> keys, AsyncCallback callback, object state)
        {
            var commands = new List<KeyValuePair<ServerConnection, Command>>();
            foreach (var key in keys)
            {
                var svr = serverPool.GetServer(key);
                commands.Add(new KeyValuePair<ServerConnection, Command>(svr, new GetCommand(key)));
            }

            return BeginExecuteCommands(commands, callback, state);
        }

        /// <summary>
        /// Gets all of the values from the cache with the given key(s). We also return the check-and-set
        /// values for those keys.
        /// </summary>
        /// <param name="keys">The list to keys you want to fetch.</param>
        /// <param name="casValues">On output, we'll populate this with the check-and-set values for the
        /// keys that we found.</param>
        /// <returns>A map of key-&gt;object for the keys we found.</returns>
        public IDictionary<string, object> Get(IEnumerable<string> keys, out IDictionary<string, long> casValues)
        {
            var res = BeginGet(keys, null, null);
            return EndGet(res, out casValues);
        }

        /// <summary>
        /// Gets all of the values from the cache with the given key(s).
        /// </summary>
        /// <param name="keys">The list to keys you want to fetch.</param>
        /// <returns>A map of key-&gt;object for the keys we found.</returns>
        public IDictionary<string, object> Get(IEnumerable<string> keys)
        {
            var res = BeginGet(keys, null, null);
            return EndGet(res);
        }

        /// <summary>
        /// Gets the value with the given key. If there's no value at the given key, null is returned.
        /// </summary>
        /// <param name="key">The key you want to fetch.</param>
        /// <param name="casValue">On output, contains the check-and-set value for the value we
        /// found (if any).</param>
        public object Get(string key, out long casValue)
        {
            var res = BeginGet(new[] { key }, null, null);

            IDictionary<string, long> casValues;
            var values = EndGet(res, out casValues);

            object obj;
            if (values.TryGetValue(key, out obj) && casValues.TryGetValue(key, out casValue))
            {
                return obj;
            }

            casValue = 0;
            return null;
        }

        /// <summary>
        /// Gets the value with the given key. If there's no value at the given key, null is returned.
        /// </summary>
        /// <param name="key">The key you want to fetch.</param>
        public object Get(string key)
        {
            var res = BeginGet(new[] { key }, null, null);
            var values = EndGet(res);

            object obj;
            if (values.TryGetValue(key, out obj))
                return obj;
            return null;
        }

        /// <summary>
        /// This is an overload of <see cref="EndGet(IAsyncResult, out IDictionary{K,V})"/> that doesn't
        /// return the check-and-set values (if you're not interested in those).
        /// </summary>
        /// <param name="res">The <see cref="IAsyncResult"/> object.</param>
        /// <returns>An <see cref="IDictionary{TKey,TValue}"/> which maps keys to a 
        /// <see cref="StoreResult"/> if there was a problem storing the value.</returns>
        public IDictionary<string, object> EndGet(IAsyncResult res)
        {
            var objects = new SortedDictionary<string, object>();

            foreach (var kvp in EndExecuteCommands(res))
            {
                var cmd = (GetCommand)kvp.Value;

                var keysEnum = cmd.Keys.GetEnumerator();
                var valuesEnum = cmd.Values.GetEnumerator();

                while (keysEnum.MoveNext() && valuesEnum.MoveNext())
                {
                    if (valuesEnum.Current != null)
                    {
                        objects[keysEnum.Current] = valuesEnum.Current;
                    }
                }
            }

            return objects;
        }

        /// <summary>
        /// Completes a "get" command. Returns a mapping of key->object with all of the keys
        /// that were available. Keys that could not be 
        /// </summary>
        /// <param name="res">The <see cref="IAsyncResult"/> object.</param>
        /// <param name="casValues">An <see cref="IDictionary{TKey,TValue}"/> that'll contain the
        /// check-and-set values for all of the keys we found.</param>
        /// <returns>An <see cref="IDictionary{TKey,TValue}"/> which maps keys to a 
        /// <see cref="StoreResult"/> if there was a problem storing the value.</returns>
        public IDictionary<string, object> EndGet(IAsyncResult res, out IDictionary<string, long> casValues)
        {
            var objects = new SortedDictionary<string, object>();
            casValues = new SortedDictionary<string, long>();

            foreach (var kvp in EndExecuteCommands(res))
            {
                var cmd = (GetCommand)kvp.Value;

                var keysEnum = cmd.Keys.GetEnumerator();
                var valuesEnum = cmd.Values.GetEnumerator();
                var casValuesEnum = cmd.CasValues.GetEnumerator();

                while (keysEnum.MoveNext() && valuesEnum.MoveNext() && casValuesEnum.MoveNext())
                {
                    if (valuesEnum.Current != null)
                    {
                        objects[keysEnum.Current] = valuesEnum.Current;
                        casValues[keysEnum.Current] = casValuesEnum.Current;
                    }
                }
            }

            return objects;
        }
        #endregion

        #region ExecuteCommands
        /// <summary>
        /// This is the main methods that does the work of executing command(s) on a list
        /// of servers.
        /// </summary>
        /// <param name="cmds">The collection of command/servers to execute.</param>
        /// <param name="callback">The method to call when we're done (if any).</param>
        /// <param name="state">The state object to pass to the callback.</param>
        /// <returns>An <see cref="IAsyncResult"/> you can pass to <b>EndExecuteCommands</b>.</returns>
        private IAsyncResult BeginExecuteCommands(IEnumerable<KeyValuePair<ServerConnection, Command>> cmds, AsyncCallback callback, object state)
        {
            var context = new ExecuteContext {
                    Callback = callback,
                    AsyncState = state,
                    AsyncWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset),
                    CompletedSynchronously = true,
                };
            context.Commands = new List<KeyValuePair<ServerConnection, ExecuteCommandContext>>(
                        from cmd in cmds select new KeyValuePair<ServerConnection, ExecuteCommandContext> (
                            cmd.Key, new ExecuteCommandContext {
                                Command = cmd.Value,
                                ParentContext = context,
                                Connection = cmd.Key,
                            }));

            foreach (var kvp in context.Commands)
            {
                kvp.Key.BeginExecuteCommand(kvp.Value.Command, OnCommandComplete, kvp.Value);
            }

            return context;
        }

        /// <summary>
        /// This is called when <em>one</em> of the commands in the list of commands you passed to
        /// <see cref="BeginExecuteCommand"/> is finished. If this was the last one, we'll set the
        /// event and call the final callback (if any).
        /// </summary>
        private void OnCommandComplete(IAsyncResult res)
        {
            var cmdContext = res.AsyncState as ExecuteCommandContext;
            if (cmdContext == null)
                return;

            var context = cmdContext.ParentContext;

            try
            {
                cmdContext.Connection.EndExecuteCommand(res);

                lock (context)
                {
                    // mark this one complete and if it's the last one, we're all done!
                    cmdContext.IsComplete = true;

                    bool allComplete = true;
                    foreach (var kvp in context.Commands)
                    {
                        if (!kvp.Value.IsComplete)
                        {
                            allComplete = false;
                            break;
                        }
                    }

                    if (allComplete)
                    {
                        // if we're all done, then mark us as such!
                        context.MarkComplete(res.CompletedSynchronously);
                    }
                }
            }
            catch (Exception e)
            {
                context.Exception = e;
                context.MarkComplete(res.CompletedSynchronously);
            }
        }

        /// <summary>
        /// This is called to end the command.
        /// </summary>
        /// <param name="res">The <see cref="IAsyncResult"/> you got from <b>BeginExecuteCommands</b>.</param>
        /// <returns>The original collection of <b>ServerConfig</b>/<b>Command</b>s, which will have been
        /// updated by the <b>ServerConnection</b> with the response.</returns>
        private IEnumerable<KeyValuePair<ServerConnection, Command>> EndExecuteCommands(IAsyncResult res)
        {
            var context = res as ExecuteContext;
            if (context == null)
                yield break;

            if (!context.IsCompleted)
                context.AsyncWaitHandle.WaitOne();

            if (context.Exception != null)
                throw new Exception("Exception occured during command execute", context.Exception);

            foreach (var kvp in context.Commands)
            {
                yield return new KeyValuePair<ServerConnection, Command>(kvp.Key, kvp.Value.Command);
            }
        }

        /// <summary>
        /// The <see cref="IAsyncResult"/> implementation we use to keep track of the
        /// </summary>
        private class ExecuteContext : IAsyncResult
        {
            /// <summary>
            /// The <see cref="AsyncCallback"/> we'll use to call the caller back when we're done.
            /// </summary>
            public AsyncCallback Callback { get; set; }

            /// <summary>
            /// The state of the command, we'll pass it back to the callback when we're done.
            /// </summary>
            public object AsyncState { get; set; }

            /// <summary>
            /// The <see cref="WaitHandle"/> we'll fire when the operation is complete.
            /// </summary>
            public WaitHandle AsyncWaitHandle { get; set; }

            /// <summary>
            /// A value which indicates whether the commmand completed synchronously or not.
            /// </summary>
            public bool CompletedSynchronously { get; set; }

            /// <summary>
            /// A flag that indicates whether the command is complete or not.
            /// </summary>
            public bool IsCompleted { get; private set; }

            /// <summary>
            /// Gets or sets the list of commands we've executed.
            /// </summary>
            public List<KeyValuePair<ServerConnection, ExecuteCommandContext>> Commands { get; set; }

            /// <summary>
            /// If this is non-<c>null</c>, it's the <see cref="Exception"/> that we'll
            /// throw at the end of all this...
            /// </summary>
            public Exception Exception { get; set; }

            /// <summary>
            /// Marks this command complete.
            /// </summary>
            public void MarkComplete(bool completeSynchronously)
            {
                IsCompleted = true;
                CompletedSynchronously = CompletedSynchronously && completeSynchronously;
                ((EventWaitHandle)AsyncWaitHandle).Set();

                // if there's a callback, call it now
                if (Callback != null)
                    Callback(this);
            }
        }

        /// <summary>
        /// This class contains the context information for a single command in the
        /// <see cref="ExecuteContext"/>.
        /// </summary>
        private class ExecuteCommandContext
        {
            public ExecuteCommandContext()
            {
                IsComplete = false;
                Command = null;
            }

            /// <summary>
            /// The main <see cref="ExecuteContext"/> we're part of.
            /// </summary>
            public ExecuteContext ParentContext { get; set; }

            /// <summary>
            /// The <see cref="ServerConnection"/> we were executed on.
            /// </summary>
            public ServerConnection Connection { get; set; }

            /// <summary>
            /// Gets the <see cref="Command"/> we're executing.
            /// </summary>
            public Command Command { get; set; }

            /// <summary>
            /// Whether this command is complete or not.
            /// </summary>
            public bool IsComplete { get; set; }
        }
        #endregion
    }
}
